/*
 * Copyright 1993-2002 Christopher Seiwald and Perforce Software, Inc.
 *
 * This file is part of Jam - see jam.c for Copyright information.
 */

#include "precompiled.hpp"
#include "scan.hpp"

#include "jamgram.hpp"

#include <boost/assign/list_of.hpp>
#include <boost/format.hpp>

#include <map>
#include <iostream>
#include <fstream>
#include <sstream>
#include <stdexcept>

//Import the internalized Jambase source
extern const char Jambase[];

//Keyword dictionary
typedef std::map<jam_string,int> keyword_map_t;

const keyword_map_t keywords = boost::assign::map_list_of
	( "!", _BANG_t )
	( "!=", _BANG_EQUALS_t )
	( "&", _AMPER_t )
	( "&&", _AMPERAMPER_t )
	( "(", _LPAREN_t )
	( ")", _RPAREN_t )
	( "+=", _PLUS_EQUALS_t )
	( ":", _COLON_t )
	( ";", _SEMIC_t )
	( "<", _LANGLE_t )
	( "<=", _LANGLE_EQUALS_t )
	( "=", _EQUALS_t )
	( ">", _RANGLE_t )
	( ">=", _RANGLE_EQUALS_t )
	( "?=", _QUESTION_EQUALS_t )
	( "[", _LBRACKET_t )
	( "]", _RBRACKET_t )
	( "actions", ACTIONS_t )
	( "bind", BIND_t )
	( "case", CASE_t )
	( "class", CLASS_t )
	( "default", DEFAULT_t )
	( "else", ELSE_t )
	( "existing", EXISTING_t )
	( "for", FOR_t )
	( "if", IF_t )
	( "ignore", IGNORE_t )
	( "in", IN_t )
	( "include", INCLUDE_t )
	( "local", LOCAL_t )
	( "module", MODULE_t )
	( "on", ON_t )
	( "piecemeal", PIECEMEAL_t )
	( "quietly", QUIETLY_t )
	( "return", RETURN_t )
	( "rule", RULE_t )
	( "switch", SWITCH_t )
	( "together", TOGETHER_t )
	( "updated", UPDATED_t )
	( "while", WHILE_t )
	( "{", _LBRACE_t )
	( "|", _BAR_t )
	( "||", _BARBAR_t )
	( "}", _RBRACE_t );

scan_context::scan_context(const jam_string &name,bool debug_mode,
	bool save_comments) : fname_(name),debug_mode_(debug_mode), 
	scanmode_(SCAN_NORMAL), line_(1), save_comments_(save_comments)
{
	assert(!name.empty());
	if(fname_=="+")
	{
		//If the filename is "+", it means use the internal jambase.
		file_.reset(new std::istringstream(Jambase));	
	} else
	{
		//Open user file
		std::auto_ptr<std::ifstream> file(new std::ifstream);
		file->open(fname_.c_str());

		//TODO: what does this mean: strcmp( i->fname, "-" ) in yychar???
		//TODO: add more detailed error description.
		if(!file->is_open())
			throw jam_exception(boost::format("Can't open %s")%fname_);

		file_=file;
	}
}

scan_context::~scan_context()
{
}

bool scan_context::is_keyword(const jam_string &str)
{
	return keywords.find(str)!=keywords.end();
}

jam_string scan_context::get_comment(int ln) const
{
	if (!save_comments_)
		throw jam_exception("Attempt to get a comment from a comment-discarding parser!");
	comment_map_t::const_iterator pos=comments_.find(ln);
	if (pos==comments_.end())
		return boost::cref("");
	return pos->second;
}

bool scan_context::get_next_char(char *out)
{
	//Exit if file is not open and we can't open it.
	assert(file_);

	file_->get(*out);

	//Check if read has succeeded
	if (file_->fail())
		return false;

	//Increase line count if we've encountered \n
	if (*out=='\n') line_++;
	
	return true;	
}

void scan_context::unget_char()
{
	file_->unget();
	if (file_->peek()=='\n') line_--;
}

bool scan_context::has_more_chars() const
{
	assert(file_);
	return !file_->eof() && file_->good();
}

void scan_context::set_scan_mode(scan_mode n)
{
	scanmode_ = n;
}

void scan_context::get_input_stream_info(jam_string *name, int *line) const
{
	*name = fname_;
	*line = line_;
}

void scan_context::throw_error(const char *s, const YYSTYPE *yylval)
{
	std::stringstream str;	
	str<<fname_<<":"<<line_<<":";
	str<<s<<" at ";
	symdump(yylval,str);
	str<<std::endl;	
	
	throw jam_exception(str.str());
}

void scan_context::symdump(const YYSTYPE *s, std::ostream &out)
{
	switch( s->type )
	{
	case EOF:
		out<<"EOF";
		break;
	case 0:
		out<<"unknown symbol " << s->string;
		break;
	case ARG:
		out<<"argument "<<s->string;
		break;
	case STRING:
		out<<"string \""<<s->string<<"\"";
		break;
	default:
		out <<"keyword "<<s->string;
		break;
	}	
}

///////////////////////////////////////////////////////////////////////////////////////
////// Main scanner function (one of the Monster(tm) functions)
///////////////////////////////////////////////////////////////////////////////////////
int scan_context::do_eof(YYSTYPE *yylval)
{
	yylval->file = boost::cref("end-of-input"); //just in case
	yylval->line = 0;

	yylval->type = EOF;
	return EOF;
}

int scan_context::yylex(YYSTYPE *yylval)
{
	char c;
	bool is_eof;
	std::string cur_token;	

	if(!has_more_chars())
		return do_eof(yylval);

	/* Get first character (whitespace or of token) */
	is_eof = !get_next_char(&c);

	if( scanmode_ == SCAN_STRING )
	{
		yylval->file = fname_;
		yylval->line = line_;

		/* If scanning for a string (action's {}'s), look for the */
		/* closing brace.  We handle matching braces, if they match! */

		int nest = 1;

		while(!is_eof)
		{
			if(c == '{')
				nest++;

			if(c == '}' && !--nest)
				break;

			cur_token.push_back(c);
			is_eof = !get_next_char(&c);

			//Turn trailing "\r\n" sequences into plain "\n" for Cygwin
			if (c == '\n' && cur_token[cur_token.size()-2] == '\r')
			{
				cur_token.resize(cur_token.size()-1);
				assert(cur_token[cur_token.size()-1]=='\r');
				cur_token[cur_token.size()-1]='\n';
			}
		}

		//We ate the ending brace -- regurgitate it.
		if(!is_eof)
			unget_char();

		if(nest)
		{
			throw_error("unmatched {} in action block",yylval);
			assert(false); //Should never be here
		}

		yylval->type = STRING;
		yylval->string=cur_token; //Unoptimal: need to avoid copying :)
	}
	else
	{
		int inquote = 0;
		bool notkeyword;

		//Eat white space
		for( ;; )
		{
			//Skip past leading white spaces
			while(!is_eof && isspace(c))
				is_eof=!get_next_char(&c);

			//Not a comment?
			if(c != '#')
				break;

			if (!save_comments_)
			{
				//Swallow up comment line.
				while( !(is_eof = !get_next_char(&c)) && c != '\n')
					;
			} else
			{
				//Save comment
				int ln=line_;
				std::string comment;
				while( !(is_eof = !get_next_char(&c)) && c != '\n')
					comment.push_back(c);
				comments_[ln]=comment; //An off-by-one index has done it again...
			}
		}
		yylval->file = fname_;
		yylval->line = line_;

		//c now points to the first character of a token.
		if(is_eof)
			return do_eof(yylval);

		//TODO: is this comment obsolete? Hash lookup should be quite fast.
		//While scanning the word, disqualify it for (expensive)
		//keyword lookup when we can: $anything, "anything", \anything * /

		notkeyword = (c=='$');

		//look for white space to delimit word
		//quotes (") get stripped but preserve white space
		//slash (\) protects next character
		while( !is_eof && (inquote||!isspace(c)) )
		{
			if(c == '"')
			{
				//begin or end quote (")
				inquote = !inquote;
				notkeyword = true;
			}
			else if(c != '\\')
			{
				//normal char
				cur_token.push_back(c);				
			}
			else if( !(is_eof=!get_next_char(&c)) ) //This was a screened character
			{
				//\c - screened character
				cur_token.push_back(c);
				notkeyword = true;
			}
			else
			{
				//EOF
				break;
			}
			is_eof=!get_next_char(&c);			
		}

		if(inquote)
		{
			throw_error("unmatched \" in string",yylval);
			assert(false); //Should never be here
		}

		//We looked ahead a character - back it up.
		if(!is_eof)
			unget_char();

		yylval->type = ARG;

		//scan token table - try to search for keywords
		//don't scan if it's obviously not a keyword or if its
		//an alphabetic when were looking for punctuation
		//assert(!cur_token.empty()); - this assert trips on empty string literals ("").
		if(!notkeyword && !(isalpha(cur_token[0]) && scanmode_== SCAN_PUNCT) )
		{
			keyword_map_t::const_iterator pos=keywords.find(cur_token);
			if (pos!=keywords.end())
			{
				yylval->type = pos->second;
				//Notice: no need for refcounted strings - all keywords fit into 
				//string's static buffer.
				yylval->string = pos->first; //used by symdump
			}
		}

		if(yylval->type == ARG)
			yylval->string=cur_token; //Unoptimal!
	}

	//TODO: again, logging.
	if(debug_mode_)
		std::cout<<"scan ", symdump(yylval,std::cout), std::cout<<std::endl;		

	return yylval->type;
}
